using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;
using LightPersist.Exceptions;

namespace LightPersist.Storage
{
	public class XmlStorage : BaseStorage
	{
		private static readonly Encoding _encoding = Encoding.UTF8;

		private static readonly Dictionary<Type, XmlSerializer> _typeToSerialzierMap = new Dictionary<Type, XmlSerializer>();

		private string Folder
		{
			get
			{
				var folder = Path.GetFullPath(CurrentConectionString);
				if (!Directory.Exists(folder))
				{
					Directory.CreateDirectory(folder);
				}
				return folder;
			}
		}

		FileStream _lock;

		protected override void OnConnected()
		{
			base.OnConnected();
			if (!Directory.Exists(CurrentConectionString))
			{
				Directory.CreateDirectory(CurrentConectionString);
			}

			_lock = File.Open(Path.Combine(CurrentConectionString, "_locked"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
		}

		public override void Dispose()
		{
			base.Dispose();
			if (_lock != null)
			{
				_lock.Close();
				_lock = null;
			}
		}

		private string GetFileName(ObjectState state)
		{
			//LightProfiling.Lap("GetFileName...");
			if (state.ClassMetainfo == null)
			{
				var search = Directory.GetFiles(Folder, "*" + state.Identity + "*.*");
				if (search.Length != 1)
				{
					throw new StorageException("Could not find object " + state.Identity + "\r\nSearched: " + search.Length);
				}
				state.ClassMetainfoDetected = Path.GetFileName(search[0]).Split('_')[0];
				return search[0];
			}
			var r = Path.Combine(Folder, state.ClassMetainfo.Name + "_" + state.Identity + ".xml");
			//LightProfiling.Lap("GetFileName.");
			return r;
		}

		public override void CommitRemove(ObjectState state)
		{
			//LightProfiling.Lap("CommitRemove...");
			var fn = GetFileName(state);
			if (File.Exists(fn))
			{
				File.Delete(fn);
			}
			else
			{
				throw new StorageException(StrErrObjDoesNotExists);
			}
			//LightProfiling.Lap("CommitRemove.");
		}

		public override void CommitUpdate(ObjectState state)
		{
			//LightProfiling.Lap("CommitUpdate...");
			var fileName = GetFileName(state);

			using (var fileWriter = File.CreateText(fileName))
			{
				SerializeObject(state, fileWriter);
			}

#if DEBUG && !PocketPC
			var fileContent = File.ReadAllText(fileName);
			var test = new ObjectState
			           {
			           	Identity = state.Identity
			           };
			DeserializeObject(fileContent, test);
#endif

			//LightProfiling.Lap("CommitUpdate.");
		}

		public override void CommitInsert(ObjectState state)
		{
			//LightProfiling.Lap("CommitInsert...");
			var fileName = GetFileName(state);
			if (File.Exists(fileName))
			{
				throw new ApplicationException("Object already exists");
			}
			CommitUpdate(state);
			//LightProfiling.Lap("CommitInsert.");
		}

		public static void SerializeObject(ObjectState state, TextWriter sw)
		{
			//LightProfiling.Lap("SerializeObject...");
			using (var writer = XmlWriter.Create(sw, new XmlWriterSettings
			                                         {
			                                         	ConformanceLevel = ConformanceLevel.Document,
			                                         	Encoding = _encoding,
			                                         	Indent = true,
			                                         	IndentChars = "\t",
			                                         	OmitXmlDeclaration = true,
			                                         }))
			{
				writer.WriteStartDocument();
				{
					writer.WriteStartElement("Object");
					{
						//writer.WriteAttributeString("Version", state.Version.ToString());

						foreach (var pair in state.PropertiesState)
						{
							var val = pair.Value;
							if (val != null)
							{
								writer.WriteStartElement(pair.Key);
								var type = val.GetType();
								writer.WriteAttributeString("Type", SerializeTypeName(type));
								SerializeValue(writer, val, type);
								writer.WriteEndElement();
							}
						}
					}
					writer.WriteEndElement();
				}
				writer.WriteEndDocument();
			}
			//LightProfiling.Lap("SerializeObject.");
		}

		private static string SerializeTypeName(Type type)
		{
			var aqn = type.AssemblyQualifiedName;
			//aqn = Regex.Replace(aqn,@"(?i)(, Version=\d+.\d+.\d+.\d+)?(, Culture=[^,]+)?(, PublicKeyToken=([a-f0-9]+|null))?",string.Empty);
			return aqn;
		}

		private static void SerializeValue(XmlWriter writer, object value, Type type)
		{
			var serialzier = GetSerializer(type);
			var ns = new XmlSerializerNamespaces();
			ns.Add("", "");
			serialzier.Serialize(writer, value, ns);
		}

		public static void DeserializeObject(string serialized, ObjectState obj)
		{
			var doc = new XmlDocument();
			doc.LoadXml(serialized);

			var root = doc.ChildNodes.OfType<XmlElement>().First();
			// obj.Version = int.Parse(root.Attributes["Version"].InnerText);

			foreach (XmlNode node in root)
			{
				if (node.NodeType == XmlNodeType.Element)
				{
					var whatWeAreSearchingFor = node.Attributes["Type"].InnerText;
					var fieldType = Type.GetType(whatWeAreSearchingFor);
					if (fieldType != null)
					{
						var value = DeserializeValue(node, fieldType);
						obj.Set(node.Name, value);
					}
					else
					{
						//Debug.WriteLine("Field Type Unknown: " + node.OuterXml);
						throw new StorageException("Field Type Unknown: " + node.OuterXml);
					}
				}
			}
		}

		private static XmlSerializer GetSerializer(Type type)
		{
			XmlSerializer serializer;
			if (!_typeToSerialzierMap.TryGetValue(type, out serializer))
			{
				lock (_typeToSerialzierMap)
				{
					if (!_typeToSerialzierMap.TryGetValue(type, out serializer))
					{
						serializer = new XmlSerializer(type, new XmlRootAttribute("value"));
						_typeToSerialzierMap.Add(type, serializer);
					}
				}
			}
			return serializer;
		}

		private static object DeserializeValue(XmlNode node, Type type)
		{
			var serialzier = GetSerializer(type);
			var data = node.InnerXml;
			using (var ms = new MemoryStream(_encoding.GetBytes(data)))
			{
				return serialzier.Deserialize(ms);
			}
		}

		public override void LoadObjectCore(ObjectState obj)
		{
			//LightProfiling.Lap("LoadObjectCore...");

			var fileName = GetFileName(obj);
			if (!File.Exists(fileName))
			{
				throw new StorageException("Object does not exists in database: " + Path.GetFileName(fileName));
			}

			string content;
#if PocketPC
			using(var sr = new StreamReader(fileName))
			{
				content = sr.ReadToEnd();
			}
#else
			content = File.ReadAllText(fileName);
#endif
			DeserializeObject(content, obj);
			//LightProfiling.Lap("LoadObjectCore.");
		}

//		public override void LoadPropertyCore(ObjectState obj, string name)
//		{
//			var temp = new ObjectState
//			{
//				Identity = obj.Identity
//			};
//			LoadObjectCore(temp);
//			obj.Set(name, temp.Get(name));
//		}

		public override IEnumerable<Guid> GetObjectIDsCore()
		{
			foreach (var fileName in Directory.GetFiles(Folder, "*.xml"))
			{
				var match = Regex.Match(fileName, @"(?i)_(?'n'[01-9a-f-]+).xml").Groups["n"];
				if (!match.Success)
				{
					throw new ApplicationException(
						string.Format("There are file '{0}' in database. Can not determine object id from file name", Path.GetFileName(fileName)));
				}
				yield return new Guid(match.Value);
			}
		}

		public override bool IsExistsCore(ObjectState obj)
		{
			var fileName = GetFileName(obj);
			return File.Exists(fileName);
		}
	}
}